Protocol Buffers 使用

概念

proto 中的go_package 的作用?

在 Protocol Buffers 中,go_package 是一个可选的选项,用于指定生成的 Go 代码的包名和导入路径。它告诉编译器将生成的 Go 代码放在哪个包中,并且在需要时为该包添加必要的导入语句。

具体来说,go_package 的值应该是形如 “<import_path>/<package_name>” 的字符串,其中 import_path 是所生成的 Go 代码的导入路径,而 package_name 则是代码所属的包名。例如:

1
2
3
4
5
6
7
8
>syntax = "proto3";
>package mypackage;
>option go_package = "example.com/proto/mypackage;mypackage";
>
>message MyMessage {
> // ...
>}
>

这里指定了生成的 Go 代码应该位于 example.com/proto/mypackage 包中,并且该包的名称为 mypackage。当编译器生成代码时,它会自动将生成的代码保存到正确的目录,并在需要时添加 import "example.com/proto/mypackage" 语句。

go_package 字段 和 package 字段的关系和区别?

在 Protocol Buffers 中,package 字段用于指定当前 .proto 文件中定义的所有消息和服务所属的包名。而 go_package 字段则是针对生成的 Go 代码的一个特殊选项,用于指定生成的 Go 代码的包名和导入路径。

当一个 .proto 文件中定义了多个消息或服务时,它们都应该属于同一个包,这时就需要使用 package 字段来指定它们所属的包名,例如:

1
2
3
4
5
6
7
8
9
10
11
>syntax = "proto3";
>package mypackage;
>
>message MyMessage {
> // ...
>}
>
>service MyService {
> // ...
>}
>

这里指定了 MyMessage 和 MyService 都属于 mypackage 包。在生成 Go 代码时,Protoc 编译器会自动将这些消息和服务保存到正确的目录结构中,并按照 Go 语言的规范将它们组织为一个 package。

而 go_package 字段则是可选的,用于指定生成的 Go 代码所在的包名和导入路径。这个选项通常用于控制生成的 Go 代码文件的导入路径,以及生成的包的名称。例如:

1
2
3
4
5
6
7
8
>syntax = "proto3";
>package mypackage;
>option go_package = "example.com/proto/mypackage;mypackage";
>
>message MyMessage {
> // ...
>}
>

这里指定了生成的 Go 代码应该位于 example.com/proto/mypackage 包中,并且该包的名称为 mypackage。在执行 protoc 命令时,编译器会将生成的 Go 代码存储到指定的目录下,并修改生成的代码中的 import 语句,以便正确引用其他包。

–go_out参数 和 proto 文件中的 go_package 有什么联系和区别?

--go_out参数和go_package选项都是与生成Go代码相关的选项,但它们的作用不同。

--go_out参数是protoc编译器的一个选项,用于指定生成的Go文件输出的目录和前缀。可以使用该选项来自定义生成的代码的输出位置和名称,同时还可以使用--go_opt选项来设置其他选项,例如输出路径为相对路径等。

例如:

1
2
> protoc --go_out=./generated --go_opt=paths=source_relative ./proto/example.proto
>

上面的命令将会把生成的Go代码输出到./generated目录下,并且设置包名使用相对路径。

go_package选项则是在.proto文件中定义的一个选项,用于指定生成的Go代码所在的包名。这个选项中的值必须是一个合法的Go包名(就像在Go源文件中一样),并且需要和--go_out参数中指定的目录结构匹配。如果没有指定go_package选项,则默认情况下,生成的Go代码的包名将会是.proto文件中的package语句的值。

例如:

1
2
3
4
5
6
7
8
9
> syntax = "proto3";
> package mypackage;
>
> option go_package = "github.com/myusername/mypackage/proto";
>
> message MyMessage {
> // ...
> }
>

上面的例子中,go_package选项指定了生成的Go代码所在的包名为github.com/myusername/mypackage/proto。然后,使用--go_out参数指定输出目录为./generated,那么编译器将会把生成的Go代码输出到./generated/github.com/myusername/mypackage/proto目录下。

所以最终生成文件的目录 = –go_out + go_package 的值。

举例:protoc 命令为 protoc –proto_path=pb –go_out=pb –go_opt=paths=source_relative book/price.proto, 文件中 option go_package=”proto_demo/pb/book”; 最终生成的文件会在哪个目录,为什么?

执行完该命令后生成的文件会位于proto_demo/pb/book目录下,文件名为price.pb.go。这是因为在proto文件中指定了go_package选项值为”proto_demo/pb/book”,该选项会告诉编译器生成的Go代码的包名为”book”,并且使用相对于生成代码文件所在目录的路径作为包名。

而在命令行中,--go_out=pb参数指定生成的Go代码文件应该输出到当前目录下的pb子目录中,--go_opt=paths=source_relative参数指定生成的Go代码中的导入路径使用相对路径,也就是相对于生成代码文件所在目录。

因此,最终生成的文件会位于proto_demo/pb/book/price.pb.go路径下。

举例:protoc 命令为 protoc –proto_path=pb –go_out=generated –go_opt=paths=source_relative book/price.proto, 文件中 option go_package=”proto_demo/pb/book”; 最终生成的文件会在哪个目录,为什么?

protoc 命令会将 pb 目录下的 price.proto 文件编译成 Go 语言的源代码,并将生成的代码放置在 generated 目录中。其中,--proto_path=pb 参数指定了查找 .proto 文件的目录为 pb 目录,--go_out=generated 参数指定了输出目录为 generated 目录,而 --go_opt=paths=source_relative 参数则是指定生成的 Go 源文件中的包路径使用相对路径。

price.proto 文件中,option go_package="proto_demo/pb/book"; 指定了生成的 Go 源代码的包路径为 proto_demo/pb/book。因此,最终生成的文件路径应该为 generated/proto_demo/pb/book/price.pb.go。其中,proto_demopb 目录都是根据 go_package 中的值来确定的,而 price.pb.go 则是根据原始的 .proto 文件名生成的。

引用同包下的 proto

文件目录结构如下,在 proto_demo 项目下,新建一个 pb 文件夹专门存放 proto 文件。

1
2
3
4
5
6
7
|____proto_demo
| |____pb
| | |____author
| | | |____author.proto
| | |____book
| | | |____book.proto
| | | |____price.proto

price.proto 的内容如下

1
2
3
4
5
6
7
8
9
10
syntax = "proto3";

package book; // 声明 protoc buffer 的包名称

option go_package="proto_demo/pb/book"; // proto_demo 是包名称

message Price{
int64 market_price = 1;
int64 sale_price =2;
}

book.proto 的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
syntax = "proto3";

package book; // 声明 protoc buffer 的包名称

option go_package="proto_demo/pb/book"; // proto_demo 是包名称

import "book/price.proto"; // 从编译的时候 --proto_path=pb 参数下面的路径去导入(注意起始位置为 proto_path 参数的下层开始)
import "author/author.proto";

import "google/protobuf/timestamp.proto"; // 导入谷歌自带的 proto

message Book {
string title = 1;
// 引用同包的 proto
Price price = 2;
// 引用不同包的 proto, author 是包名
author.Info author= 3;
// 导入谷歌自带的 proto
google.protobuf.Timestamp date = 4;
}

service BookService{
rpc Create(Book)returns(Book);
}

在 book.proto 中引用了 price.proto 中的字段,因为 price.proto 和 book.proto 都是 package book 包,所以是同一个包,属于相同包的引用。这时只需要从起始位置为 proto_path 参数的下层开始导入即可。而且导入的 Price 字段前面不需要添加包名。

引用不同包下的 proto

author.proto 的内容如下

1
2
3
4
5
6
7
8
9
syntax = "proto3";

package author; // 声明 protoc buffer 的包名称

option go_package="proto_demo/pb/author"; // proto_demo 是包名称

message Info{
string name=1;
}

author.proto 属于 author 包,在 book中导入 author 的时候属于跨包导入,所以在 book 中导入字段需要添加包名,然后 . 字段名。例如上面的 author.Info author= 3;

引用谷歌包下的 proto

上面的 book 中引入了 google 的 Timestamp,前提是安装 Protocol Buffers 的时候将 include 文件夹也保留了,并且将 bin 下面的 protoc 添加到了环境变量。

1
2
3
#protoc
export PROTOC_DIR=/Users/rex/Documents/server/protoc-22.2-osx-x86_64
export PATH=$PATH:$PROTOC_DIR/bin

protoc-22.2-osx-x86_64 目录下的 include 就含有谷歌内置的 proto 文件。

生成 go 代码

1
protoc --proto_path=pb --go_out=pb --go_opt=paths=source_relative book/price.proto book/book.proto author/author.proto

上述命令在 proto_demo 目录下执行

生成 grpc 的代码

定义了 service 才会生成 grpc 的代码

1
protoc --proto_path=pb --go-grpc_out=pb --go-grpc_opt=paths=source_relative book/book.proto book/price.proto author/author.proto

上述命令在 proto_demo 目录下执行

使用 makefile

在 proto_demo 下新增 makefile 文件,内容如下,其中需要修改 PROTO_DIR 为自己的 proto 目录,这里为 pb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.PHONY: gen help

PROTO_DIR=pb

gen:
protoc \
--proto_path=$(PROTO_DIR) \
--go_out=$(PROTO_DIR) \
--go_opt=paths=source_relative \
--go-grpc_out=$(PROTO_DIR) \
--go-grpc_opt=paths=source_relative \
--grpc-gateway_out=$(PROTO_DIR) \
--grpc-gateway_opt=paths=source_relative \
$(shell find $(PROTO_DIR) -iname "*.proto")

help:
@echo "make gen - 生成pb及grpc代码"

解决 Goland Proto 文件导入提出找不到问题

在下面文件中,在 message Book 中,分别引入了同包的 proto 文件和 不同包的 proto 文件。但默认 goland 会提示不存在。解决方法是在 goland 的设置中 语言和框架 Protocol Buffers,不勾选 Configure automatically, 并在下面新增一个存放 –proto_path 参数的值,例如这里的 pb。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
syntax = "proto3";

package book; // 声明 protoc buffer 的包名称

option go_package="proto_demo/pb/book"; // proto_demo 是包名称

import "book/price.proto"; // 从编译的时候 --proto_path=pb 参数下面的路径去导入(注意起始位置为 proto_path 参数的下层开始)
import "author/author.proto";

message Book {
string title = 1;
// 引用同包的 proto
Price price = 2;
// 引用不同包的 proto, author 是包名
author.Info author= 3;
}

// protoc --proto_path=pb --go_out=pb --go_opt=paths=source_relative book/price.proto book/book.proto author/author.proto
// 上述命令在 proto_demo 目录下执行,语法上提示报错,但是是可以生成的。
// 解决方法是在 goland 的设置中 语言和框架 Protocol Buffers,不勾选 Configure automatically, 并在下面新增一个存放 --proto_path 参数的值,例如这里的 pb

参考文章:https://www.liwenzhou.com/posts/Go/protobuf/

本文代码:https://github.com/rexyan/Go-Microservice/tree/main/proto_demo